import sys, datetime, fnmatch, stat, re, time
from fs.base import FS, NoDefaultMeta
from fs.expose import dokan
from fs.errors import ResourceInvalidError, ResourceNotFoundError, DestinationExistsError, OperationFailedError
from fs.filelike import FileWrapper
from client.settings import DBStore
from api import KuaiPanAPI
from client.ndfile import NDFile, NDFileDownloader, NDFileUploader, NDFileMetadataGetter, NDFileMetaData
from client.ndfile import NDFileCachePool, NDFileTemporaryPool
from fs.path import pathjoin, abspath, dirname, basename, pathsplit
from error import KuaiPanError401

if sys.platform != "win32":
    from fs.expose import fuse

class KuaiPanFile(NDFile):
    def _close(self):
        if self.touched:
            self.fs.metadata.update(self.path, {'size': long(self.size)})

class KuaiPanDownloader(NDFileDownloader):
    def _get_remote_file(self, path):
        try:
            return self.fs.api.download_file(path)
        except Exception, e:
            print 'KuaiPanUploader', path,  e

class KuaiPanUploader(NDFileUploader):
    def _write_remote_file(self, path, f, size):
        try:
            self.fs.api.upload_file(self.fs.upload_url, path, f, size)
        except Exception, e:
            print 'KuaiPanUploader', path, e
        self.fs.metadata.force_get(dirname(path))


class KuaiPanMetadataGetter(NDFileMetadataGetter):
    def _get_meta(self, path):
        try:
            return self.fs.api.metadata(path)
        except Exception, e:
            print 'KuaiPanMetadataGetter', path, e
            
   
class KuaiPanMetaData(NDFileMetaData):
    def __init__(self, fs, case_insensitive=False, refresh_timeout = 120):
        super(KuaiPanMetaData, self).__init__(fs, getter_class=KuaiPanMetadataGetter, case_insensitive=case_insensitive, refresh_timeout=refresh_timeout)

    def _add(self, path, v):
        if v.get('type') is None:
            v['type'] = 'folder'

        self._update(path, v)
        if v.get('type') == 'folder':
            now = time.time()
            orginpath = v.get('_orginpath', path)
            names = []
            for f in v.get('files', []):
                name = f.get('name')
                names.append(name.upper() if self.case_insensitive else name)
                f['_timestamp'] = now
                p = f['_orignpath'] = pathjoin(orginpath, name)
                self._update(p, f)
            '''    
            if self.case_insensitive:
                path = path.upper()
            if path[-1] != '/':
                path += '/'
            for k in self.m.keys():
                if k.startswith(path) and k != path:
                    name = basename(k)
                    if self.case_insensitive:
                        name = name.upper()
                    if name not in names:
                        try:
                            del self.m[k]
                        except:
                            pass
            '''  
                        
    def get(self, path):
        if self.case_insensitive:
            p = path.upper()
        else:
            p = path
        self.lock.acquire()
        keys = self.m.keys()
        if (p not in keys) and (dirname(p) in keys):
            self.lock.release()
            return None
        self.lock.release()
        return super(KuaiPanMetaData, self).get(path)

class KuaiPanFS(FS):
    _meta = {'thread_safe' : False,
          'network' : False,
          'virtual' : False,
          'read_only' : False,
          'unicode_paths' : True,
          'case_insensitive_paths' : True,
          'atomic.makedir' : True,
          'atomic.rename' : True,
          'atomic.setcontents' : False,
         }
    
    def _str2date(self, s):
        return datetime.datetime.strptime(s, '%Y-%m-%d %H:%M:%S')
    
    def _get_time_diffrence(self):
        try:
            now = time.time()
            resp = KuaiPanAPI.openTime()
            remote_time = resp.get('Timestamp')
            return int(remote_time) - int(now)
        except:
            return 0
    
    def secret_keeper_factory(self):
        def secret_keeper(token, secret):
            self.setting['kuaipan_oauth_token'] = token
            self.setting['kuaipan_oauth_token_secret'] = secret
        return secret_keeper
    
    def __delete__(self):
        self.unmount()
        if self.metadata:
            self.metadata.stop()

    def _login(self):
        self.api.requestToken()
        self.api.authorize()
        self.api.accessToken()

    def __init__(self, username='', userpwd='', cache_dir=None, setting_db='settings.db'):
        super(KuaiPanFS, self).__init__()
        self.mp = None
        self.username = username
        self.userpwd = userpwd
        
        self.setting = DBStore(setting_db)
        api_args = {'username': username,
                    'userpwd': userpwd,
                    'difftime': self._get_time_diffrence(),
                    'consumer_key': self.setting['kuaipan_consumer_key'],
                    'consumer_secret': self.setting['kuaipan_consumer_secret'],
                    'oauth_token': self.setting['kuaipan_oauth_token'],
                    'oauth_token_secret': self.setting['kuaipan_oauth_token_secret'],
                    'secret_keeper': self.secret_keeper_factory()}
        self.api = KuaiPanAPI(**api_args)
        
        try:
            userinfo = self.api.account_info()
        except KuaiPanError401:
            self._login()
            userinfo = self.api.account_info()
        except:
            raise
        else:
            self._login()
            userinfo = self.api.account_info()
        self.username = userinfo.get('user_name')
        self.upload_url = self.api.upload_locate().get('url')
        self.user_id = userinfo.get('user_id', -1)
        self.max_file_size = userinfo.get('max_file_size', 0)
        self.quota_used = userinfo.get('quota_used', 0)
        self.quota_total = userinfo.get('quota_total', 0)
        self.driver = ''
        #self.pool = NDFileTemporaryPool(self, KuaiPanDownloader, KuaiPanUploader, cache_dir)
        self.pool = NDFileCachePool(self, KuaiPanDownloader, KuaiPanUploader, cache_dir)
        self.metadata = KuaiPanMetaData(self, case_insensitive=KuaiPanFS._meta.get('case_insensitive_paths', True))
        
        
    def getmeta(self, meta_name, default=NoDefaultMeta):
        if meta_name == 'free_space':
            return self.quota_total - self.quota_used
        elif meta_name == 'total_space':
            return self.quota_total
        
        return super(KuaiPanFS, self).getmeta(meta_name, default)
    
    def exists(self, path):
        path = abspath(path)
        return self.metadata.get(path) != None
    
    def open(self, path, mode="r"):
        path = abspath(path)
        if sys.platform == 'win32' and self.isdir(path):
            raise ResourceInvalidError(path)
        mode = filter(lambda c: c in "rwabt+-",mode)
        meta = self.metadata.get(path)
        if meta is None:
            isnew = True
            sha1 = None
            if 'w' in mode or 'a' in mode:
                size = 0
            else:
                raise ResourceNotFoundError
        else:
            isnew = False
            size = meta.get('size', 0)
            sha1 = meta.get('sha1')
        f = KuaiPanFile(self, self.metadata.get_real_path(path), mode, self.pool, size, isnew, sha1)
        return FileWrapper(f, mode)
        
    def isdir(self, path):
        path = abspath(path)
        meta = self.metadata.get(path)
        if meta is None:
            return False
        return meta.get('type') == 'folder'

    def isfile(self, path):
        path = abspath(path)
        meta = self.metadata.get(path)
        if meta is None:
            return False
        return meta.get('type') == 'file'

    def listdir(self, path="./", wildcard=None, full=False, absolute=False, dirs_only=False, files_only=False):
        path = abspath(path)
        if dirs_only and files_only:
            raise ValueError("dirs_only and files_only can not both be True")

        meta = self.metadata.get(path)
        if meta is None:
            raise ResourceNotFoundError
        
        files = meta.get('files')
        if files is None:
            meta = self.metadata.force_get(path)
            files = meta.get('files')
            if files is None:
                raise ResourceInvalidError
            
        entries = []
        for f in files:
            n = f.get('name', '')
            t = f.get('type')
            p = pathjoin(path, n)
            if not f.get('is_deleted', False):
                if t == 'folder':
                    if not files_only:
                        entries.append(n)
                elif t == 'file':
                    if not dirs_only:
                        entries.append(n)
            
        if wildcard is not None:
            if not callable(wildcard):
                wildcard_re = re.compile(fnmatch.translate(wildcard))
                wildcard = lambda fn:bool (wildcard_re.match(fn))
            entries = [p for p in entries if wildcard(p)]

        if full:
            entries = [pathjoin(path, p) for p in entries]
        elif absolute:
            entries = [abspath(pathjoin(path, p)) for p in entries]
        return entries

    def _mkdir(self, path):
        if self.exists(path):
            raise DestinationExistsError(path)
        try:
            r = self.api.fileops_create_folder(self.metadata.get_real_path(path))
            if r.get('msg') == 'ok':
                self.metadata.force_get(dirname(path))
        except Exception, e:
            print 'makedir', path, e
            raise ResourceInvalidError
        
    def _mkdirs(self, path):
        head, tail = pathsplit(path)
        while not tail:
            head, tail = pathsplit(path)
        if head and tail and not self.exists(head):
            try:
                self._mkdirs(head)
            except DestinationExistsError:
                pass
            except Exception:
                raise
        self.mkdir(path)
    
    def makedir(self, path, recursive=False, allow_recreate=False):
        path = abspath(path)
        try:
            if recursive:
                self._mkdirs(path)
            else:
                self._mkdir(path)
        except DestinationExistsError:
            if self.isfile(path):
                raise ResourceInvalidError(path)
            if not allow_recreate:
                raise DestinationExistsError(path)
        except:
            raise

    def remove(self, path):
        path = abspath(path)
        try:
            r = self.api.fileops_delete(self.metadata.get_real_path(path))
            if r.get('msg') == 'ok':
                self.metadata.delitem(path) 
                self.metadata.force_get(dirname(path))
        except Exception, e:
            print 'remove', e
            raise ResourceInvalidError

    def removedir(self, path, recursive=False, force=False):
        path = abspath(path)
        try:
            r = self.api.fileops_delete(self.metadata.get_real_path(path))
            if r.get('msg') == 'ok':
                self.metadata.delitems(path)
                self.metadata.force_get(dirname(path))
        except Exception, e:
            print 'removedir', e
            raise ResourceInvalidError

    def rename(self, src, dst):
        src = abspath(src)
        dst = abspath(dst)
        try:
            r = self.api.fileops_move(self.metadata.get_real_path(dst), self.metadata.get_real_path(src))
            if r.get('msg') == 'ok':
                self.metadata.delitems(src)
                self.metadata.force_get(dirname(src))
        except Exception, e:
            print 'rename', e
            raise ResourceInvalidError
            
    def move(self, src, dst, overwrite=False, chunk_size=16384):
        src = abspath(src)
        dst = abspath(dst)

        if not self.isfile(src):
            if self.isdir(src):
                raise ResourceInvalidError(src, msg="Source is not a file: %(path)s")
            raise ResourceNotFoundError(src)
        if not overwrite and self.exists(dst):
            raise DestinationExistsError(dst)
        
        try:
            r = self.api.fileops_move(self.metadata.get_real_path(dst), self.metadata.get_real_path(src))
            if r.get('msg') == 'ok':
                self.metadata.delitems(src)
                self.metadata.force_get(dirname(src))
                self.metadata.force_get(dirname(dst))
        except Exception, e:
            print 'move', e
            raise ResourceInvalidError
        

    def movedir(self, src, dst, overwrite=False, ignore_errors=False, chunk_size=16384):
        src = abspath(src)
        dst = abspath(dst)
        
        print src, dst, overwrite, ignore_errors
        if not self.isdir(src):
            if self.isfile(src):
                raise ResourceInvalidError(src, msg="Source is not a directory: %(path)s")
            raise ResourceNotFoundError(src)
        if not overwrite and self.exists(dst):
            raise DestinationExistsError(dst)

        try:
            r = self.api.fileops_move(self.metadata.get_real_path(dst), self.metadata.get_real_path(src))
            if r.get('msg') == 'ok':
                self.metadata.delitems(src)
                self.metadata.force_get(dirname(src))
                self.metadata.force_get(dirname(dst))
        except Exception, e:
            print 'movedir', e
            if not ignore_errors:
                raise ResourceInvalidError

    def getinfo(self, path):
        path = abspath(path)
        meta = self.metadata.get(path)
        if meta is None:
            raise ResourceNotFoundError
        
        t = meta.get('type')
        files = meta.get('files')
        if t == 'folder':
            if files is None:
                self.metadata.add_queue(path)
            else:
                for f in files:
                    p = pathjoin(path, f.get('name'))
                    m = self.metadata.get(p)
                    if m and m.get('type') == 'folder' and m.get('files') is None:
                        self.metadata.add_queue(p)

        info = {'name': basename(path),
                'size': long(meta.get('size', 0)),
                'st_mode' : 0777 | (stat.S_IFDIR if t == 'folder' else stat.S_IFREG)}

        ct = meta.get('create_time')
        if ct is not None:
            ct = self._str2date(ct)
            info['created_time'] = ct
            
        mt = meta.get('modify_time')
        if mt is not None:
            mt = self._str2date(mt)
            info['modified_time'] = mt

        return info
    
    def getsize(self, path):
        path = abspath(path)
        meta = self.metadata.get(path)
        if meta is None:
            raise OperationFailedError("get size of resource", path)
        
        size = meta.get('size', None)
        if size is None:
            raise OperationFailedError("get size of resource", path)
        return size

    def unmount(self):
        if self.mp is not None:
            if sys.platform == 'win32':
                dokan.unmount(self.driver)
            else:
                fuse.unmount(self.path)
            self.mp = None
            
    def mount(self, driver='M:\\', path='/mnt/M', numthreads=1, fsname='NetDisk FS', volname=None):
        self.unmount()
        if volname is None:
            volname = 'KuaiPan(%s)' % self.username
        if sys.platform == 'win32':
            flags = dokan.DOKAN_OPTION_REMOVABLE
            self.mp = dokan.mount(self, driver, foreground=True, numthreads=numthreads, flags=flags, fsname=fsname, volname=volname)
        else:
            self.path = path
            self.mp = fuse.mount(self, driver, foreground=True, fsname=fsname)

if __name__=="__main__":        
    
    db_path = 'test_kuaipan_fs.db'
    db = DBStore(db_path)
    db['kuaipan_consumer_key'] = 'xcABUF6z7RcvaWv7'
    db['kuaipan_consumer_secret'] = 'gGAqN4PoUyGqbx1K'
    db['kuaipan_username'] = 'test@egopie.com'
    db['kuaipan_userpwd'] = 'test#egopie.com'
    db['kuaipan_cache_dir'] = 'd:/tmp'
    try:
        kuaipanfs = KuaiPanFS(db['kuaipan_username'], db['kuaipan_userpwd'], db['kuaipan_cache_dir'], db_path)
        kuaipanfs.mount()
    except:
        raise
    

